﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;

namespace Uniriotec.DC.CoordDist.Base
{

    public class Process
    {
        // Normalmente não haverá necessidade de alterar diretamente essa classe.
        // Utilize a classe SampleProcess, no projeto da Exclusão Mútua Distribuída
        // como exemplo de como implementar seu próprio processo.
        
        // Ciclo de vida normal de um processo:
        // Start -> Init -> DoWork -> DoNormalWork (11 vezes)

        // Ciclo de uma mensagem chegando:
        // OnClientConnect -> OnDataReceived -> OnMessageReceived

        // Ciclo de uma mensagem sendo enviada
        // SendToAll ou SendMessage -> Send -> SendCallback
        public event EventHandler<MessageReceivedEventArgs> MessageReceived;

        #region Private Variables

        private int id = 0;
        private int port = 0;
        private int time = 0;
        private int higherTime = 0;
        private List<int> nodeIDs = new List<int>();
        private Socket sockListener;
        private Socket sockWorker;
        private int totalProcs = 0;

        #endregion

        #region Properties

        public List<int> NodeIDs
        {
            get { return nodeIDs; }
        }

        public int Id
        {
            get { return id; }
            set { id = value; }
        }

        public int TotalProcs
        {
            get { return totalProcs; }
            set { totalProcs = value; }
        }

        public int Time
        {
            get { return time; }
        }

        #endregion

        #region Initialization

        protected virtual void Init()
        {
            MessageReceived += new EventHandler<MessageReceivedEventArgs>(OnMessageReceived);

            sockListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint local = new IPEndPoint(IPHelper.GetIP("localhost"), port);
            sockListener.Bind(local);
            sockListener.Listen(200);
            sockListener.BeginAccept(new AsyncCallback(OnClientConnect), sockListener);

            for (int i = 1; i <= totalProcs; i++)
                nodeIDs.Add(i);
        }

        public void Start(object startParameters)
        {
            StartProcessParameters parameters = (StartProcessParameters)startParameters;
            this.id = parameters.Id;
            this.higherTime = 1;
            this.port = parameters.Port;
            this.totalProcs = parameters.TotalProcs;

            Init();
            DoWork();
        }

        #endregion

        #region Helper Methods

        protected void Debug(string str)
        {
            Console.WriteLine("(Process " + id + ") " + str);
        }

        protected int GetPort(int process)
        {
            return port - (id - process);
        }

        protected int GetProcessNumber(int remotePort)
        {
            return remotePort - (GetPort(id) - id);
        }

        #endregion

        #region Connection

        private void OnClientConnect(IAsyncResult asyn)
        {
            try
            {
                sockWorker = sockListener.EndAccept(asyn);
                StateObject state = new StateObject();
                state.workSocket = sockWorker;
                sockWorker.BeginReceive(state.buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback(OnDataReceived), state);
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        #endregion

        #region Receive Data

        protected virtual void OnMessageReceived(object sender, MessageReceivedEventArgs e){}

        private void OnDataReceived(IAsyncResult asyn)
        {
            StateObject so = (StateObject)asyn.AsyncState;
            int read = sockWorker.EndReceive(asyn);

            if (read > 0)
            {
                so.sb.Append(Encoding.ASCII.GetString(so.buffer, 0, read));
                so.workSocket.BeginReceive(so.buffer, 0, StateObject.BufferSize, 0,
                                         new AsyncCallback(OnDataReceived), so);
            }
            else
            {
                if (so.sb.Length > 1)
                {
                    //All of the data has been read
                    string strContent;
                    strContent = so.sb.ToString();

                    string[] partitions = strContent.Split('|');
                    if (partitions.Length > 3)
                        throw new Exception("Wrong Message (proc " + id + "): " + strContent);

                    Message msg = new Message(Convert.ToInt32(partitions[0]), Convert.ToInt32(partitions[1]), partitions[2]);

                    if (msg.Time > higherTime)
                        higherTime = msg.Time;

                    #region raise MessageReceived Event

                    try
                    {
                        EventHandler<MessageReceivedEventArgs> handler = MessageReceived;

                        if (handler != null)
                        {
                            MessageReceivedEventArgs e = new MessageReceivedEventArgs();
                            e.Message = msg;

                            handler(this, e);
                        }
                    }
                    catch
                    {
                        // Handle exceptions here
                    }

                    #endregion

                }
                so.workSocket.Close();
                sockListener.BeginAccept(new AsyncCallback(OnClientConnect), sockListener);
            }

        }

        #endregion

        #region Send Data

        protected void SendToAll(Message message)
        {
            Debug("sent to all: " + message);

            foreach (int nodeId in nodeIDs)
            {
                if (nodeId == id)
                    continue;
                SendMessage(message, nodeId);
            }
        }

        protected void SendToAll(string text) 
        {
            Debug("sent to all: " + text);

            foreach (int nodeId in nodeIDs)
            {
                if (nodeId == id)
                    continue;
                SendMessage(text, id, nodeId);
            }
        
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="text">The message text</param>
        /// <param name="from">Local Process Number</param>
        /// <param name="processNumber">Remote Process Number</param>
        protected void SendMessage(string text, int from, int remoteProcessNumber)
        {
            Message message = new Message(from, text);
            SendMessage(message, remoteProcessNumber);
        }


        protected void SendMessage(Message message, int remoteProcessNumber)
        {
            message.Time = time;

            Debug("TO: " + remoteProcessNumber + " sent : " + message.ToString());

            int remotePort = GetPort(remoteProcessNumber);

            Socket senderSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            senderSocket.Connect("localhost", remotePort);
            Send(senderSocket, message.ToString());
        }

        #region Private Methods

        private static void Send(Socket handler, String data)
        {
            // Convert the string data to byte data using ASCII encoding.
            byte[] byteData = Encoding.ASCII.GetBytes(data);

            // Begin sending the data to the remote device.
            handler.BeginSend(byteData, 0, byteData.Length, 0,
            new AsyncCallback(SendCallback), handler);
        }

        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket handler = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                int bytesSent = handler.EndSend(ar);

                handler.Shutdown(SocketShutdown.Both);
                handler.Close();
            }
            catch (Exception e)
            {
                throw e;
            }
        }

        #endregion

        #endregion

        #region Misc Algorithm Methods

        protected void ChooseTime()
        {
            time = higherTime + 1;
        }

        #endregion

        #region Work Methods

        protected virtual void DoNormalWork()
        {
            Random rnd = new Random();
            System.Threading.Thread.Sleep(rnd.Next(20, 4000));
        }

        protected virtual void DoWork()
        {
            DoNormalWork();

            int j = 0;
            while (j++ < 10)
            {
                DoNormalWork();
            }

            Debug("finished doing work");
        }

        #endregion

    }


}

